---
title: MVCC
description: MVCC 是乐观锁思想的一种实现方式，MVCC 的核心是 Undo Log 和 Read View，MV就是通过 Undo Log 来保存历史数据，实现多版本的管理，CC 是通过 Read View 来实现管理，通过 Read View 原则来决定数据是否显示。同时针对不同的隔离级别，Read View 的生成策略不同，也就实现了不同的隔离级别。
---

我们知道事务有四种隔离级别，以及可能存在的三种异常问题，如下图：

![](https://img.wkq.pub/pic/c2e9a4ce5793b031f3846890d0f6189b.webp)

在 MySQL 中，默认的隔离级别是可重复读，可以解决脏读和不可重复读的问题，但不能解决幻读的问题。如果我们想要解决幻读问题，就需要采用串行话的方式，但这样一来就会大幅降低数据库的事务并发能力

实际上 MVCC 机制的设计，就是通过乐观锁的方式来解决不可重复读和幻读问题的，它可以在大多数情况下替代行级锁，降低系统的开销。

![](https://img.wkq.pub/pic/568bb507e1edb431d8121a2cb5c7caa0.webp)

## MVCC 是什么

MVCC 的全程是 Multiversion Concurrent Control，MVCC 是通过数据行的多个版本管理来实现数据库的并发控制技术，简单来说它的思想就是保存数据的历史版本。这样我们就可以通过比较版本号决定数据是否显示出来，读取数据的时候不需要加锁也可以保证事务的隔离效果。

通过 MVCC 可以解决以下几个问题：

1. 读写之间阻塞的问题，通过 MVCC 可以让读写互相不阻塞，即读不阻塞写，写不阻塞读，这样就可以提升事务的并发能力。
2. 降低了死锁的概率。这是因为 MVCC 采用了 乐观锁的机制，读取数据时并不需要加锁，对于写操作，也只锁定必要行。
3. 解决了一致性读的问题。一致性读也被称为快照读，当我们查询数据库在某个时间点的快照时，只能看到这个时间点之前事务提交更新的结果，而不能看到这个时间点之后事务提交的更新结果。

## 什么是快照读，什么是当前读

快照都读取的是快照数据。不加锁的简单的 SELECT 都属于快照读，比如这样：

```sql
SELECT * FROM player WHERE ...
```

当前读就是读取最新数据，而不是历史版本的数据。加锁的 SELECT ，或者对数据进行增删改都会进行当前读，比如：

```sql
SELECT * FROM player LOCK IN SHARE MODE;
```

```sql
SELECT * FROM player FOR UPDATE;
```

```sql
INSERT INTO player values ...
```

```sql
DELETE FROM player WHERE ...
```

```sql
UPDATE player SET ...
```

**快照读就是普通的读操作，而当前读包括了加锁的读取和 DML 操作。**



## InnoDB 中的 MVCC 是如何实现的？

MVCC 没有正式的标准，所以在不同的 DBMS 中，MVCC 的实现方式可能是不同的。下面是 InnoDB 中的 MVCC 的实现。

### 事务版本号
每开启一个事务，我们都会从数据库中获得一个事务 ID（也就是事务版本号），这个事务ID 是自增长的，通过 ID 大小，可以判断事务的时间顺序。

### 行记录的隐藏列
InnoDB 的叶子段存储了数据页，数据页中保存了行记录，而在行记录中有一些重要的隐藏字段，如下图所示：
1. db_row_id: 隐藏的行 ID，用来生成默认聚簇索引。如果我们创建数据表的时候没有指定聚簇索引，这时 InnoDB 就会用这个隐藏列 ID 来创建聚簇索引。采用聚簇索引的方式可以提升数据的查询效率。
2. db_trx_id: 操作这个数据的事务 ID，也就是最后一个对该数据进行插入或跟新的事务ID。
3. db_roll_ptr: 回滚指针，也就是指向这个记录的 Undo Log 信息。

![](https://img.wkq.pub/pic/d15a5d0a313492b208d2aad410173b20.webp)

### Undo Log

InnoDB 将行快照记录保存在了 Undo Log 里，我们可以在 回滚段中找到它们，如下图所示：

![4799c77b8cdfda50e49a391fea727281](https://img.wkq.pub/pic/4799c77b8cdfda50e49a391fea727281.webp)

回滚指针将数据行的所有快照记录都通过链表的结构串联了起来，每个快照的记录都保存了当时的 db_trx_id, 也就是那个时间段操作这个数据的事务 ID。这样如果我们想要找历史快照，就可以通过遍历回滚指针的方法进行查找。

### Read View 是如何工作的

在 MVCC 机制中，多个事务对同一个记录进行更新会产生多个历史快照，这些历史快照保存在 Undo Logo 中。如果一个事务想要查询这个行记录，需要读取那个版本的行记录呢？这时候就需要用到 Read View 了，他帮助我们解决了行的可见性问题。Read View 保存了当前事务开启时所有活跃（还没有提交）的事务列表。

:::tip

Read View  保存了不该让这个事务看到的其它事务 ID 列表。

:::

在 Read View 中有几个重要的属性：

1. trx_ids ,系统当前正在活跃的事务 ID 集合。
2. low_limit_id，活跃的事务中最大的事务 ID

3. up_limit_id: 活跃的事务中最小的事务 ID
4. creator_trx_id：创建这个视图的事务 ID

如下图所示： trx_ids 为 trx2、trx3、trx5 和 trx8 的集合，活跃的最大事务 ID（low_limit_id）为 trx8 ，活跃的最小事务 ID (up_limit_id) 为 trx2。

![a7fe7d4a0e25fce469c3d00e5e3ec600](https://img.wkq.pub/pic/a7fe7d4a0e25fce469c3d00e5e3ec600.webp)

假设当前有事务 creator_trx_id 想要读取某个行记录，这个行记录的事务 ID 为 trx_id,那么会出现以下几种情况。

如果 trx_id < 最小活跃事务 ID （up_limit_id） ,这说明该行记录在这些活跃的事务创建之间就已经提交了，那么这个行记录对该事务是可见的。

如果 trx_id > 最大活跃事务 ID （low_limit_id）,这说明该行记录在这些活跃的事务创建之后才创建，那么这个行记录对当前事务不可见。

如果 up_limit_id < trx_id <low_limit_id ,这说明该行记录所在事务 trx_id 在目前 creator_trx_id 这个事务创建的时候，可能还处于活跃状态，因此我们需要在 trx_ids 集合中进行遍历，如果 trx_id 存在于 trx_ids 集合中，证明这个事务 trx_id 还处于活跃状态。不可见，否则，如果 trx_id 不存在于 trx_ids 集合中，证明事务 trx_id 已经提交了，该行记录可见。



查询一条行记录的时候，系统如何通过多版本并发控制技术找到它：

1. 首先获取事务自己的版本号，也就是事务 ID；
2. 获取 Read View ；
3. 查询得到的数据，然后与 Read View 中的事务 版本号进行比较；
4. 如果不符合 ReadView 规则，就需要从 Undo Logo 中获取历史快照；
5. 最后返回 符合规则的数据。

在 InnoDB 中 ，MVCC 是通过 Undo + Read View 进行数据读取， Undo Log 保存了历史快照，而 Read View 规则帮我们判断当前版本的数据是否可见。

在隔离级别为读已提交（Read Commit ）时，一个事务的每一次 SELECT 查询都会获取一次  Read View 。

![5dfd6b7491484ad49efc5f08b214bf5c](https://img.wkq.pub/pic/5dfd6b7491484ad49efc5f08b214bf5c.webp)

在读已提交的隔离级别下，同样的查询语句都会重新获取一次 Read View ，这时如果 Read View 不同，就 可能产生不可重复读或幻读的问题。

当隔离级别为可重复读的时候，就避免了不可重复读，这是因为一个事务只在第一次  SELECT 的时候获取一次 Read View ，而后面所有的 SELECT 都会复用这个 Read View ，如下表所示：

![7b9c80fb892f858e08a64fc8ef7257f2](https://img.wkq.pub/pic/7b9c80fb892f858e08a64fc8ef7257f2.webp)

## InnoDB 是如何解决幻读的

在可重复读的情况下，InnoDB 可以通过 Next-Key 锁+ MVCC 来解决幻读问题。



在读已提交的情况下，即使采用了 MVCC 也会出现幻读。如果我们同时开启事务 A 和事务 B ，现在事务 A 中进行某个条件范围的查询，读取的时候采用排它锁，在事务 B 中增加一条符合该条件范围的数据，并进行提交，然后我们在事务 A 中再次查询该条件范围的数据，就会发现结果集中多出一条符合条件的数据，这样就出现了幻读。

![4f112ba8411db336f58c8db9c9e8f0cb](https://img.wkq.pub/pic/4f112ba8411db336f58c8db9c9e8f0cb.webp)

出现幻读的原因是在读已提交 的情况下，InnoDB 只采用记录锁（Record Locking）。这里简单介绍下 InnoDB 三种行锁的方式：

1. 记录锁：针对单个行记录添加锁；
2. 间隙锁（Gap Locking）： 可以帮助我们锁住一个范围（索引之间的间隙），但不包括记录本身。采用间隙锁的方式可以防止幻读情况的产生。
3. Next-Key 锁：帮我们锁住一个范围，同时锁住记录本身，相当于间隙锁+记录锁，可以解决幻读的问题。

在隔离级别为可重复读时， InnoDB 会采用 Next-Key 锁的机制，帮我们解决幻读问题。

## 总结

MVCC 的核心就是 Undo Log + Read View，MV 就是通过 Undo Log 来保存数据的历史版本，实现多版本的管理， “CC“是 通过 Read View 来实现管理，通过 Read  View  原则来决定数据是否显示。同时针对不同的隔离级别， Read View 的生成策略不同，也就实现了不同的隔离级别。

